/*
 * Copyright @ 2010 Radomir Mladenovic and BugsIO Solutions
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 *       
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.gjutil.caching.aop;

import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import com.googlecode.gjutil.caching.CacheProvider;
import com.googlecode.gjutil.caching.CacheProvidersRegistry;
import com.googlecode.gjutil.caching.DefaultCacheProvidersRegistry;
import com.googlecode.gjutil.caching.MemCacheCleanup;
import com.googlecode.gjutil.caching.MemCacheable;
import com.googlecode.gjutil.caching.MemCacheSetter;
import com.googlecode.gjutil.util.HashCodeCacheKey;
import com.googlecode.gjutil.util.HashCodeCalculator;
import com.googlecode.gjutil.util.Objects;

public class CachingInterceptor implements MethodInterceptor {

	private static final Logger log = Logger.getLogger(CachingInterceptor.class.getName());

	private CacheProvidersRegistry registry; 

	public CachingInterceptor()
	{
	}
	
	public CachingInterceptor(CacheProvidersRegistry registry)
	{
		this.registry = registry;
	}
	
	public CachingInterceptor(CacheProvider cacheProvider)
	{
		this.registry = new DefaultCacheProvidersRegistry();
		this.registry.setDefaultCacheProvider(cacheProvider);
	}
	
	public Object invoke(MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();
		try {
			// TODO later we could cleanup on exit and check if there's time for it or return immediately 
			MemCacheCleanup memCacheCleanup = method.getAnnotation(MemCacheCleanup.class);
			if(memCacheCleanup != null) {
				handle(invocation, memCacheCleanup);
			}		
			MemCacheable memCacheable = method.getAnnotation(MemCacheable.class);
			if(memCacheable != null) {
				return handleMemCacheable(invocation, memCacheable);
			}
			MemCacheSetter setter = method.getAnnotation(MemCacheSetter.class);
			if(setter != null) {
				return handleMemCachedSetter(invocation, setter);
			}
		}
		catch(Exception e) {
			log.log(Level.SEVERE, "Error handling cache related annotation.", e);
		}
		Object rv = invocation.proceed();
		return rv;
	}

	private Object handleMemCacheable(MethodInvocation invocation, MemCacheable options) throws Throwable {
		Object key = generateKey(invocation.getThis(), invocation.getMethod(), options.group(), invocation.getArguments());
		String cacheName = getCacheName(options.cache(), options.group());
		Object result = getFromCache(key, cacheName);
		if (result != null) // FIXME any purpose of caching nulls? method will always be invoked!
			return result;
		result = invocation.proceed();
		putInCache(key, result, cacheName, options.expirationSeconds(), null);
		return result;		
	}
	
	private Object handleMemCachedSetter(MethodInvocation invocation, MemCacheSetter options) throws Throwable {
		Object[] args = invocation.getArguments();
		if(args.length < 2) { // FIXME is this ok? do we always need one argument for key generation?
			throw new IllegalArgumentException("MemCacheSetter annotation requires at least 2 arguments on method " +
					invocation.getMethod());
		}
		Object key = generateKey(invocation.getThis(), null, options.group(), args, 0, args.length - 1);
		// the last argument is an object we're storing to cache
		Object value = args[args.length - 1];

		String cacheName = getCacheName(options.cache(), options.group());
		putInCache(key, value, cacheName, options.expirationSeconds(), Boolean.TRUE);
		
		// do we have a use case for invoking the original method?
		return null;
	}
	
	private void handle(MethodInvocation invocation, MemCacheCleanup options) throws Throwable {
		cleanupAllCaches();
	}
	
	public void cleanupAllCaches() {
		for (CacheProvider cache : registry.getAllCacheProviders()) {
			log.severe("Call cache cleanup for " + cache);
			cache.cleanup();
		}		
	}
	
	private String getCacheName(String cache, String group) {
		if(cache != null && cache.length() > 0)
			return cache;
		if(group != null && group.length() > 0)
			return group;
		return null;
	}
	
	protected Object generateKey(Object target, Method method, String key, Object[] methodArguments) {
		return generateKey(target, method, key, methodArguments, 
				0, methodArguments != null ? methodArguments.length : 0);
	}
	
	protected Object generateKey(Object target, Method method, String group, 
			Object[] methodArguments, int beginIndex, int endIndex) {
		HashCodeCalculator hashCodeCalculator = new HashCodeCalculator();
		
		if(group != null && group.length() > 0)
			hashCodeCalculator.append(group.hashCode());
		else if(method != null)
			hashCodeCalculator.append(System.identityHashCode(method));

		if (methodArguments != null) {
			for (int i = beginIndex; i < endIndex; i++) {
				Object methodArgument = methodArguments[i];
				int hash = 0;
				// if (generateArgumentHashCode) {
				// hash = Reflections.reflectionHashCode(methodArgument);
				// } else {
				hash = Objects.nullSafeHashCode(methodArgument);
				// }
				hashCodeCalculator.append(hash);
			}
		}

		long checkSum = hashCodeCalculator.getCheckSum();
		int hashCode = hashCodeCalculator.getHashCode();

		Object cacheKey = new HashCodeCacheKey(checkSum, hashCode);
		return cacheKey;
	}

	protected Object getFromCache(Object key, String cache) {
		try {
			CacheProvider cacheProvider = registry.getCacheProvider(cache);
			if(cacheProvider != null) {
				Object value = cacheProvider.getFromCache(key);
				if(log.isLoggable(Level.FINE)) {				
					if(value != null)
						log.fine("Found in cache " + cacheProvider.getName() + " key " + key);
					else
						log.fine("Not found in cache " + cacheProvider.getName() + " key " + key);
				}
				return value;
			}
			else {				
				log.log(Level.WARNING, "There's no cache provider for " + cache);
			}			
		} catch (Throwable t) {
			log.log(Level.WARNING, "Error retrieving object from cache.", t);
		}
		return null;
	}

	protected void putInCache(Object key, Object value, String cache, int expirationSeconds, Boolean setAlways) {
		try {
			CacheProvider cacheProvider = registry.getCacheProvider(cache);
			if(cacheProvider != null) {
				cacheProvider.putInCache(key, value, expirationSeconds, setAlways);
				if(log.isLoggable(Level.FINE)) {
					log.fine("Stored to cache " + cacheProvider.getName() + " key " + key);
				}
			}
			else {				
				log.log(Level.WARNING, "There's no cache provider for group " + cache);
			}			
		} catch (Throwable t) {
			log.log(Level.WARNING, "Error storing object to cache.", t);
		}
	}

	public CacheProvidersRegistry getRegistry() {
		return registry;
	}

	public void setRegistry(CacheProvidersRegistry registry) {
		this.registry = registry;
	}
}
